import { observer } from "mobx-react";
import { types } from "mobx-state-tree";

import RequiredMixin from "../../mixins/Required";
import PerRegionMixin from "../../mixins/PerRegion";
import InfoModal from "../../components/Infomodal/Infomodal";
import Registry from "../../core/Registry";
import SelectedModelMixin from "../../mixins/SelectedModel";
import VisibilityMixin from "../../mixins/Visibility";
import Tree from "../../core/Tree";
import Types from "../../core/Types";
import { guidGenerator } from "../../core/Helpers";
import ControlBase from "./Base";
import { AnnotationMixin } from "../../mixins/AnnotationMixin";
import { cn } from "../../utils/bem";
import "./Choices/Choices.scss";

import "./Choice";
import DynamicChildrenMixin from "../../mixins/DynamicChildrenMixin";
import { FF_LSDV_4583, isFF } from "../../utils/feature-flags";
import { ReadOnlyControlMixin } from "../../mixins/ReadOnlyMixin";
import SelectedChoiceMixin from "../../mixins/SelectedChoiceMixin";
import ClassificationBase from "./ClassificationBase";
import PerItemMixin from "../../mixins/PerItem";
import Infomodal from "../../components/Infomodal/Infomodal";
import { useMemo } from "react";
import { Select, Tooltip } from "@humansignal/ui";

/**
 * The `Choices` tag is used to create a group of choices, with radio buttons or checkboxes. It can be used for single or multi-class classification. Also, it is used for advanced classification tasks where annotators can choose one or multiple answers.
 *
 * Choices can have dynamic value to load labels from task. This task data should contain a list of options to create underlying `<Choice>`s. All the parameters from options will be transferred to corresponding tags.
 *
 * The `Choices` tag can be used with any data types.
 *
 * @example
 * <!--Basic text classification labeling configuration-->
 * <View>
 *   <Choices name="gender" toName="txt-1" choice="single-radio">
 *     <Choice alias="M" value="Male" />
 *     <Choice alias="F" value="Female" />
 *     <Choice alias="NB" value="Nonbinary" />
 *     <Choice alias="X" value="Other" />
 *   </Choices>
 *   <Text name="txt-1" value="John went to see Mary" />
 * </View>
 *
 * @example <caption>This config with dynamic labels</caption>
 * <!--
 *   `Choice`s can be loaded dynamically from task data. It should be an array of objects with attributes.
 *   `html` can be used to show enriched content, it has higher priority than `value`, however `value` will be used in the exported result.
 * -->
 * <View>
 *   <Audio name="audio" value="$audio" />
 *   <Choices name="transcription" toName="audio" value="$variants" />
 * </View>
 * <!-- {
 *   "data": {
 *     "variants": [
 *       { "value": "Do or doughnut. There is no try.", "html": "<img src='https://labelstud.io/images/logo.png'>" },
 *       { "value": "Do or do not. There is no trial.", "html": "<h1>You can use hypertext here</h2>" },
 *       { "value": "Do or do not. There is no try." },
 *       { "value": "Duo do not. There is no try." }
 *     ]
 *   }
 * } -->
 *
 * @example <caption>is equivalent to this config</caption>
 * <View>
 *   <Audio name="audio" value="$audio" />
 *   <Choices name="transcription" toName="audio" value="$variants">
 *     <Choice value="Do or doughnut. There is no try." />
 *     <Choice value="Do or do not. There is no trial." />
 *     <Choice value="Do or do not. There is no try." />
 *     <Choice value="Duo do not. There is no try." />
 *   </Choices>
 * </View>
 * @name Choices
 * @meta_title Choices Tag for Multiple Choice Labels
 * @meta_description Customize Label Studio with multiple choice labels for machine learning and data science projects.
 * @param {string} name                - Name of the group of choices
 * @param {string} toName              - Name of the data item that you want to label
 * @param {single|single-radio|multiple} [choice=single] - Single or multi-class classification
 * @param {boolean} [showInline=false] - Show choices in the same visual line
 * @param {boolean} [required=false]   - Validate whether a choice has been selected
 * @param {string} [requiredMessage]   - Show a message if validation fails
 * @param {region-selected|no-region-selected|choice-selected|choice-unselected} [visibleWhen] - Control visibility of the choices. Can also be used with the `when*` parameters below to narrow down visibility
 * @param {string} [whenTagName]       - Use with `visibleWhen`. Narrow down visibility by name of the tag. For regions, use the name of the object tag, for choices, use the name of the `choices` tag
 * @param {string} [whenLabelValue]    - Use with `visibleWhen="region-selected"`. Narrow down visibility by label value. Multiple values can be separated with commas
 * @param {string} [whenChoiceValue]   - Use with `visibleWhen` (`"choice-selected"` or `"choice-unselected"`) and `whenTagName`, both are required. Narrow down visibility by choice value. Multiple values can be separated with commas
 * @param {boolean} [perRegion]        - Use this tag to select a choice for a specific region instead of the entire task
 * @param {boolean} [perItem]          - Use this tag to select a choice for a specific item inside the object instead of the whole object
 * @param {string} [value]             - Task data field containing a list of dynamically loaded choices (see example below)
 * @param {boolean} [allowNested]      - Allow to use `children` field in dynamic choices to nest them. Submitted result will contain array of arrays, every item is a list of values from topmost parent choice down to selected one.
 * @param {select|inline|vertical} [layout] - Layout of the choices: `select` for dropdown/select box format, `inline` for horizontal single row display, `vertical` for vertically stacked display (default)
 */
const TagAttrs = types.model({
  toname: types.maybeNull(types.string),
  showinline: types.maybeNull(types.boolean),
  choice: types.optional(types.enumeration(["single", "single-radio", "multiple"]), "single"),
  layout: types.optional(types.enumeration(["select", "inline", "vertical"]), "vertical"),
  value: types.optional(types.string, ""),
  allownested: types.optional(types.boolean, false),
});

const Model = types
  .model({
    pid: types.optional(types.string, guidGenerator),

    visible: types.optional(types.boolean, true),

    type: "choices",
    children: Types.unionArray(["choice", "view", "header", "hypertext"]),
  })
  .views((self) => ({
    get shouldBeUnselected() {
      return self.choice === "single" || self.choice === "single-radio";
    },

    states() {
      return self.annotation.toNames.get(self.name);
    },

    get serializableValue() {
      const choices = self.selectedValues();

      if (choices && choices.length) return { choices };

      return null;
    },

    get preselectedValues() {
      return self.tiedChildren.filter((c) => c.selected === true && !c.isSkipped).map((c) => c.resultValue);
    },

    get selectedLabels() {
      return self.tiedChildren.filter((c) => c.sel === true && !c.isSkipped);
    },

    selectedValues() {
      return self.selectedLabels.map((c) => c.resultValue);
    },

    get defaultChildType() {
      return "choice";
    },

    // perChoiceVisible() {
    //     if (! self.whenchoicevalue) return true;

    //     // this is a special check when choices are labeling other choices
    //     // may need to show
    //     if (self.whenchoicevalue) {
    //         const choicesTag = self.annotation.names.get(self.toname);
    //         const ch = choicesTag.findLabel(self.whenchoicevalue);

    //         if (ch && ch.selected)
    //             return true;
    //     }

    //     return false;
    // }
  }))
  .actions((self) => ({
    afterCreate() {
      // TODO depricate showInline
      if (self.showinline === true) self.layout = "inline";
      if (self.showinline === false) self.layout = "vertical";
    },

    needsUpdate() {
      if (self.result) self.setResult(self.result.mainValue);
      else self.setResult([]);
    },

    requiredModal() {
      InfoModal.warning(self.requiredmessage || `Checkbox "${self.name}" is required.`);
    },

    // this is not labels, unselect affects result, so don't unselect on random reason
    unselectAll() {},

    updateFromResult(value) {
      self.setResult(Array.isArray(value) ? value : [value]);
    },

    // unselect only during choice toggle
    resetSelected() {
      self.selectedLabels.forEach((c) => c.setSelected(false));
    },

    setResult(values) {
      self.tiedChildren.forEach((choice) => {
        let isSelected = false;

        if (!choice.isSkipped) {
          isSelected = values?.some?.((value) => {
            if (Array.isArray(value) && Array.isArray(choice.resultValue)) {
              if (value.length !== choice.resultValue.length) return false;
              return value.every?.((val, idx) => val === choice.resultValue?.[idx]);
            }
            return value === choice.resultValue;
          });
        }

        choice.setSelected(isSelected);
      });
    },
  }))
  .actions((self) => {
    const Super = {
      validate: self.validate,
    };

    return {
      validate() {
        if (!Super.validate() || (self.choice !== "multiple" && self.checkResultLength() > 1)) return false;
      },

      checkResultLength() {
        const _resultFiltered = self.children.filter((c) => c._sel);

        return _resultFiltered.length;
      },

      beforeSend() {
        if (self.choice !== "multiple" && self.checkResultLength() > 1)
          Infomodal.warning(
            `The number of options selected (${self.checkResultLength()}) exceed the maximum allowed (1). To proceed, first unselect excess options for:\r\n • Choices (${
              self.name
            })`,
          );
      },
    };
  });

const ChoicesModel = types.compose(
  "ChoicesModel",
  ControlBase,
  ClassificationBase,
  SelectedModelMixin.props({ _child: "ChoiceModel" }),
  RequiredMixin,
  PerRegionMixin,
  ...(isFF(FF_LSDV_4583) ? [PerItemMixin] : []),
  ReadOnlyControlMixin,
  SelectedChoiceMixin,
  VisibilityMixin,
  DynamicChildrenMixin,
  AnnotationMixin,
  TagAttrs,
  Model,
);

const ChoicesSelectLayout = observer(({ item }) => {
  const options = useMemo(
    () =>
      item.tiedChildren.map((i) => ({
        value: i._value,
        label: (
          <Tooltip title={i.hint}>
            <span data-testid="choiceOptionText" className="w-full">
              {i._value}
            </span>
          </Tooltip>
        ),
      })),
    [item.tiedChildren],
  );
  return (
    <Select
      style={{ width: "100%" }}
      value={item.selectedLabels.map((l) => l._value)}
      multiple={item.choice === "multiple"}
      disabled={item.isReadOnly()}
      onChange={(val) => {
        if (Array.isArray(val)) {
          item.resetSelected();
          val.forEach((v) => item.findLabel(v).setSelected(true));
          item.updateResult();
        } else {
          const c = item.findLabel(val);

          if (c) {
            c.toggleSelected();
          }
        }
      }}
      options={options}
    />
  );
});

const HtxChoices = observer(({ item }) => {
  return (
    <div
      className={cn("choices")
        .mod({ hidden: !item.isVisible || !item.perRegionVisible(), layout: item.layout })
        .toClassName()}
      ref={item.elementRef}
    >
      {item.layout === "select" ? <ChoicesSelectLayout item={item} /> : Tree.renderChildren(item, item.annotation)}
    </div>
  );
});

Registry.addTag("choices", ChoicesModel, HtxChoices);

export { HtxChoices, ChoicesModel, TagAttrs };
